package wortubung.util;

import java.awt.Toolkit;
import java.awt.datatransfer.StringSelection;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Array;
import java.text.Normalizer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import java.util.regex.Pattern;

import static java.lang.Math.min;
import static wortubung.lang.de.DeutschSpracheWerkzeug.MFNP_NAMES;

public class CommonUtils {

    public static final String FILE_ENCODING = "UTF8";
    public static final String NL = "\r\n";

    public static final String[] EMPTY_STRING_ARRAY = new String[0];

	public static final Random random = new Random();

	private static Printable debugOut = null;

	public static void setDebugOut(Printable p) {
		debugOut = p;
	}

	public static void debug(Object... vals) {
		if (debugOut != null) {
    		debugOut.prompt("     <DEBUG> ");
    		debugOut.promptln(vals);
		}
	}

	public static String getClassSimpleName(Object obj) {
    	return getClassSimpleName(obj, '.');
    }

    public static String getClassSimpleName(Object obj, char sep) {
    	Class<?> clz = (obj instanceof Class) ? ((Class<?>)obj) : obj.getClass();
		Class<?> outerClass = clz.getEnclosingClass();
		return (outerClass == null) ? clz.getSimpleName() : (outerClass.getSimpleName() + sep + clz.getSimpleName());
    }

    public static String yesNo(boolean b) { return b ? "yes" : "no"; }

    public static String onOff(boolean b) { return b ? "on" : "off"; }

    public static boolean checkBoolean(String value) {
        return (value==null) ||
        		"true".equalsIgnoreCase(value) ||
        		"on".equalsIgnoreCase(value) ||
        		"yes".equalsIgnoreCase(value);
    }

    public static int sum(int[] nums) {
    	int ret = 0;
    	if (nums != null) {
    		for (int n : nums)
    			ret += n;
    	}
    	return ret;
    }

    public static int maxLen(Class<?> enumClass) {
    	if (!enumClass.isEnum())
    		return 0;
    	int ret = 1;
    	for (Object o : enumClass.getEnumConstants()) {
    		int len = o.toString().length();
    		if (len > ret)
    			ret = len;
    	}
    	return ret;
    }

    public static int maxLen(int initValue, String[] strs) {
    	if (strs != null) {
        	for (String s : strs)
        		if (s != null && length(s) > initValue)
        			initValue = s.length();
    	}
    	return initValue;
    }

    public static int maxLen(int initValue, Collection<String> strs) {
    	if (strs != null) {
        	for (String s : strs)
        		if (s != null && length(s) > initValue)
        			initValue = s.length();
    	}
    	return initValue;
    }

    public static <T> boolean eq(T o1, T o2, boolean nullsEq) {
        if (o1 == null && o2 == null)
            return nullsEq;
        if (o1 == null || o2 == null)
            return false;
        return o1.equals(o2);
    }

    public static <T> boolean contains(T[] arr, T val) {
	    for (T v : arr)
			if (eq(v, val, false))
				return true;
		return false;
    }

    public static <T> boolean arrayEq(T[] a1, T[] a2) {
    	if (length(a1) != length(a2))
    		return false;
    	for (int i=length(a1)-1; i>=0; --i)
    		if (!eq(at(a1, i), at(a2, i), true))
    			return false;
    	return true;
    }

    public static <T> boolean array2dEq(T[][] aa1, T[][] aa2) {
    	int len1 = length(aa1);
    	int len2 = length(aa2);
		if (len1 != len2)
    		return false;
    	for (int i=len1-1; i>=0; --i)
    		if (!arrayEq(at(aa1, i), at(aa2, i)))
    			return false;
    	return true;
    }

    public static boolean isNotEmpty(Object o) {
        return !isEmpty(o);
    }

    public static String trim(String s) {
    	if (s == null)
    		return null;
    	return s.trim();
    }

    public static void trim(String[] sa) {
    	if (sa == null)
    		return;
    	for (int i=sa.length-1; i>=0; --i)
   			sa[i] = trim(sa[i]);
    }

    public static <T> T[][] shrink(T[][] src) {
    	int len = length(src);
    	if (len == 0)
    		return src;
    	int i=len-1;
    	for (; i>=0 && src[i] == null; --i);
    	++i;
    	if (i == len)
    		return src;
    	if (i == 0)
    		return null;
    	T[][] ret = newArray((Class<T[][]>)src.getClass(), i);
    	for (--i; i>=0; --i)
    		ret[i] = src[i];
    	return ret;
    }

    public static boolean isEmpty(Object o) {
        if (o == null)
            return true;
        
        String s;
        if (o instanceof String) {
            s = (String)o;
        } else {
            if (o.getClass().isArray())
                return Array.getLength(o) == 0;
            if (o instanceof Collection)
                return ((Collection<?>)o).size() == 0;
            if (o instanceof Map)
                return ((Map<?,?>)o).keySet().size() == 0;
            s = String.valueOf(o);
        }
        return s == null || s.trim().length() == 0;
    }

    public static int length(Object o) {
        if (o == null)
            return 0;
        
        if (o instanceof String)
            return ((String)o).length();

        if (o.getClass().isArray())
            return ((Object[])o).length;

        if (o instanceof Collection)
            return ((Collection<?>)o).size();

        if (o instanceof Map)
            return ((Map<?,?>)o).keySet().size();

        return String.valueOf(o).length();
    }

    public static boolean isAllNull(String[] sa) {
    	if (sa == null)
    		return true;
		boolean isAllNull = true;
		for (String s : sa) {
			if (s != null) {
				isAllNull = false;
				break;
			}
		}
		return isAllNull;
    }

    public static String[] prependTo(String prefix, String[] strs) {
    	if (prefix != null && strs != null) {
    		for (int i=0; i<length(strs); ++i)
    			strs[i] = prefix + strs[i];
    	}
   		return strs;
    }

    public static String prepend(String value, Object... values) {
    	if (value == null) {
    		switch (values.length) {
    		case 0: return "";
    		case 1: return neverNull(values[1]);
    		}
    	}

    	return append(append(new StringBuilder(), values), value).toString();
    }

    public static StringBuilder append(StringBuilder sb, Object... values) {
    	for (Object val : values)
    		if (val != null)
    			sb.append(val);
    	return sb;
    }

    public static StringBuilder appendWithPrefix(StringBuilder sb, String prefix, Object value) {
        appendWithPrefixSuffix(sb, prefix, value, null);
        return sb;
    }

    public static StringBuilder appendWithPrefixSuffix(StringBuilder sb, String prefix, Object value, String suffix) {
        if (isNotEmpty(value)) {
            if (prefix != null)
                sb.append(prefix);
            if (value != null)
                sb.append(value);
            if (suffix != null)
                sb.append(suffix);
        }
        return sb;
    }

    public static void trimTrailing(StringBuilder sb, String pattern) {
        int idx = sb.lastIndexOf(pattern);
        if (idx >= 0 && idx == sb.length() - length(pattern))
            sb.setLength(idx);
    }

    public static String neverNull(Object s) {
        return (s == null) ? "" : s.toString();
    }

    public static String neverNull(Object s, String defaultValue) {
        return (s == null) ? defaultValue : s.toString();
    }

    public static String neverEmpty(String s, String emptyString) {
        return isEmpty(s) ? emptyString : s;
    }

    public static String getBefore(String line, String sep) {
    	int idx = line.indexOf(sep);
    	if (idx < 0)
    		return line;
    	return line.substring(0, idx);
    }

    public static String getAfter(String line, String sep) {
    	int idx = line.indexOf(sep);
    	if (idx < 0)
    		return null;
    	return line.substring(idx + sep.length());
    }

    public static String toString(Object o) {
        try {
            return String.valueOf(o);
        } catch(NullPointerException e) {
            return null;
        }
    }

    public static String concat(char separator, Object[] strs) {
    	if (strs == null)
    		return null;
    	StringBuilder sb = new StringBuilder();
    	boolean first = true;
    	for (Object s : strs) {
    		if (first)
    			first = false;
    		else
    			sb.append(separator);
    		if (s != null)
    			sb.append(s.toString());
    	}
   		return sb.toString();
    }

    public static String concat(Object... strs) {
    	switch (strs.length) {
    	case 0: return "";
    	case 1: return strs[0] == null ? "": strs[0].toString();
    	}
    	
    	StringBuilder sb = new StringBuilder();
    	for (Object s : strs)
    		if (s != null)
    			sb.append(s.toString());
   		return sb.toString();
    }

    public static String[] concat(String[]... arrays) {
        return concat(String[].class, arrays);
    }

    public static <T> T[] newArray(Class<T[]> clz, int len) {
        return clz.cast(Array.newInstance(clz.getComponentType(), len));
    }

    public static <T> T[] concat(Class<T[]> clz, T[]... arrays) {
        int len = 0;
        for (T[] a : arrays)
        	len += length(a);
        T[] ret = newArray(clz, len);
        int ptr = 0;
        for (T[] a : arrays) {
            if (a == null)
                continue;
            System.arraycopy(a,  0, ret, ptr, a.length);
            ptr += a.length;
        }
        return ret;
    }

    public static <T> T[] copyOf(T[] src) {
    	return (src == null) ? null : Arrays.copyOf(src, src.length);
    }

    public static <T> T[] augment(T first, T[] src) {
    	if (isEmpty(src))
    		return null;
    	T[] ret = newArray((Class<T[]>)src.getClass(), src.length+1);
    	ret[0] = first;
    	for (int i=src.length-1; i>=0; --i)
    		ret[i+1] = src[i];
    	return ret;
    }

    public static String[] pick(String[] src, boolean[] includes) {
    	if (includes == null)
    		return src;
		int i = MFNP_NAMES.length;
		for (boolean b : includes)
			if (!b)
				i--;
		String[] ret = new String[i];
		int idx = 0;
		for (i=0; i<src.length; ++i) {
			if (includes[i])
				ret[idx++] = src[i];
		}
		return ret;
	}

    public static <T> String concatEvenly(String prefix, boolean prefixFirstLineOnly, int totalWidth, int maxStrLen, String separator, T... strs) {
    	prefix = neverNull(prefix);
    	separator = neverNull(separator);
    	if (separator.length() == 0)
    		separator = " ";
    	int displayWidth = isEmpty(prefix) ? totalWidth : (totalWidth - prefix.length());
    	if (maxStrLen <= 0) {
        	for (Object o : strs) {
        		int len = isEmpty(o) ? 0 : o.toString().length();
        		if (len > maxStrLen)
        			maxStrLen = len;
        	}
    	}
    	int cols = displayWidth / (maxStrLen + separator.length());
    	StringBuilder sb = new StringBuilder();
    	int cnt = 0;
    	for (Object o : strs) {
    		String s = makeStringLen(neverNull(o), 0, maxStrLen, ' ');
    		if (cnt == 0) {
    			sb.append(prefix).append(s);
    			if (prefixFirstLineOnly)
    				prefix = makeLine(' ', prefix.length());
    		} else if (cnt % cols == 0) {
   				sb.append(NL).append(prefix).append(s);
    		} else {
   				sb.append(separator).append(s);
    		}
    		++cnt;
    	}
    	return sb.toString();
    }

	public static boolean at(boolean[] arr, int idx) {
    	return (arr == null) || (idx >= arr.length) || idx < 0
    			? false : arr[idx];
	}

	public static int at(int[] arr, int idx, int defaultVal) {
    	return (arr == null) || (idx >= arr.length) || idx < 0
    			? defaultVal : arr[idx];
	}

    public static <T> T at(T[] arr, int idx) {
    	return at(arr, idx, null);
    }

    public static <T> T at(T[] arr, int idx, T defaultVal) {
    	return (arr == null) || (idx >= arr.length) || idx < 0 || arr[idx] == null
    			? defaultVal : arr[idx];
    }

    public static <T> T at(T[][] arr, int i1, int i2) {
    	return at(arr, i1, i2, null);
    }

    public static <T> T at(T[][] arr, int i1, int i2, T defaultVal) {
    	return (arr == null) || (i1 >= arr.length) || (arr[i1] == null) || (i2 >= arr[i1].length) || arr[i1][i2] == null
    			? defaultVal : arr[i1][i2];
    }

    public static int numDigits(int number) {
        int digits = 1;
        int factor = 10;
        while (number / factor > 0) {
            ++digits;
            factor *= 10;
        }
        return digits;
    }

    public static String toRomanDigit(int number) {
    	final String[] digits = { "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X" };
   		return digits[number];
    }

    public static String[] separate(String line, String separator) {
        if (isEmpty(line))
            return EMPTY_STRING_ARRAY;

        String[] arr = line.split(separator);
        for (int i=0; i<arr.length; ++i)
           arr[i] = arr[i].trim();
        return arr;
    }

    public static int compare(String s1, String s2) {
        if (s1 != null)
            return s2 != null ? s1.compareTo(s2) : 1;
        else
            return s2 != null ? -1 : 0;
    }

    public static int compareIgnoreCase(String s1, String s2) {
        if (s1 != null)
            return s2 != null ? s1.toLowerCase().compareTo(s2.toLowerCase()) : 1;
        else
            return s2 != null ? -1 : 0;
    }

    public static int compare(boolean b1, boolean b2) {
        return (b1 == b2) ? 0 : (b1 ? 1 : -1);
    }

    public static String makeLine(char ch, int len) {
    	return makeStringLen(null, 0, len, ch);
    }

    public static String makeStringLen(String str, int spaces, int totalLen, char filler) {
    	str = neverNull(str);
        if (str.length() >= totalLen)
            return str;

        StringBuilder sb = new StringBuilder(str);
        int cnt = spaces;
        while (cnt > 0 && sb.length() < totalLen) {
            sb.append(' ');
            --cnt;
        }
        while (sb.length() < totalLen)
            sb.append(filler);
        return sb.toString();
    }

    public static String rightFlush(int width, String msg) {
		return makeLine(' ', width - getWidth(msg)) + neverNull(msg);
    }

    public static String leftFlush(int width, Object msg) {
		return makeStringLen(neverNull(msg), 0, width, ' ');
    }

	public static void appendf(StringBuilder sb, String fmt, Object... vals) {
		sb.append(String.format(fmt, vals));
	}

    public static List<String> wrapText(String text, int lineWidth, List<String> result) {
    	if (result == null)
    		result = new ArrayList<String>();
    	
    	if (length(text) <= lineWidth) {
    		result.add(text);
    	} else {
    		StringBuffer sb = new StringBuffer();
    		for (String seg : text.split(" +")) {
    			if (sb.length() + seg.length() <= lineWidth) {
    				sb.append(seg).append(' ');
    			} else {
    				if (sb.charAt(sb.length()-1) == ' ')
    					sb.setLength(sb.length()-1);
    				result.add(sb.toString());
    				sb.setLength(0);
    				while (seg.length() > lineWidth) {
    					result.add(seg.substring(0, lineWidth));
    					seg = seg.substring(lineWidth);
    				}
    				if (seg.length() > 0)
    					sb.append(seg).append(' ');
    			}
    		}
    		if (sb.length() > 0) {
				if (sb.charAt(sb.length()-1) == ' ')
					sb.setLength(sb.length()-1);
    			result.add(sb.toString());
    		}
    	}
    	return result;
    }

    public static <T> void randomize(T[] arr) {
    	int len = arr.length;
    	for (int i=0; i<len; ++i) {
    		int idx1 = random.nextInt(len);
    		int idx2 = random.nextInt(len);
    		T tmp = arr[idx1];
    		arr[idx1] = arr[idx2];
    		arr[idx2] = tmp;
    	}
    }

    @SuppressWarnings("unchecked")
	public static <T> T[] randomize(Collection<T> coll) {
    	T[] oa = (T[])coll.toArray();
    	randomize(oa);
    	return oa;
    }

    public static <T> void incCount(Map<T,Integer> map, T t) {
    	addCount(map, t, 1);
    }

    public static <T> void addCount(Map<T,Integer> map, T t, int delta) {
    	Integer cnt = map.get(t);
    	if (cnt == null)
    		cnt = delta;
    	else
    		cnt += delta;
    	map.put(t, cnt);
    }

    public static String encryptAnswer(int ans) {
    	int i = ((int)System.currentTimeMillis()) >> (random.nextInt(5)) & 0xFFFF;
    	int j = i << 16;
    	switch(i>>6 & 0x03) {
    	case 0: i = i & 0xF00F | ans << 4; break;
    	case 1: i = i & 0xF80E | ans << 3; break;
    	case 2: i = i & 0xFC03 | ans << 2; break;
    	case 3: i = i & 0xFE01 | ans << 1; break;
    	}
    	return Integer.toHexString((j | i) & 0x7FFFFFFF);
    }

    public static int decryptAnswer(String hexStr) {
    	int i = Integer.parseInt(hexStr, 16);
    	switch(i>>22 & 0x03) {
    	case 0: i >>= 4; break;
    	case 1: i >>= 3; break;
    	case 2: i >>= 2; break;
    	case 3: i >>= 1; break;
    	}
    	return i & 0x0FF;
    }
	// newline
	public static StringBuilder nl(StringBuilder sb, Object... values) {
		boolean hasValue = false;
		for (Object v : values)
			if (v != null) {
				hasValue = true;
				break;
			}
		if (hasValue && sb.length() > 0)
			sb.append(NL);
		for (Object v : values)
			if (v != null)
				sb.append(v);
		return sb;
	}

    public static String getStackTrace(Throwable t) {
        StringWriter errors = new StringWriter();
        t.printStackTrace(new PrintWriter(errors));
        return errors.toString();
    }

	public static void getRelevantStackTrace(StringBuilder sb, Throwable t) {
		if (t == null)
			return;
		nl(sb, t.getClass().getName(), " - ", t.getMessage());
		ArrayList<StackTraceElement> stes = new ArrayList<StackTraceElement>();
		int clsLen = 10;
		int mthdLen = 5;
		boolean started = false;
		for (StackTraceElement ste : t.getStackTrace()) {
			String clsName = ste.getClassName();
			if (!started)
				started = true;
			else if (!clsName.startsWith("wortubung."))
				continue;
			if (clsName.length() > clsLen)
				clsLen = clsName.length();
			if (ste.getMethodName().length() > mthdLen)
				mthdLen = ste.getMethodName().length();
			stes.add(ste);
		}

		String fmt1 = "    at %-" + clsLen + "s :: %-" + mthdLen + "s()";
		String fmt2 = fmt1 + " [line %d]";
		for (StackTraceElement ste : stes) {
			int lineNum = ste.getLineNumber();
			if (lineNum > 0)
				nl(sb, String.format(fmt2, ste.getClassName(), ste.getMethodName(), lineNum));
			else
				nl(sb, String.format(fmt1, ste.getClassName(), ste.getMethodName()));
		}
	}

	public static InputStream readFromCP(String path) throws IOException {
		if (path.startsWith("/"))
			path = path.substring(1);
		return Thread.currentThread().getContextClassLoader().getResourceAsStream(path);
	}

	public static Properties readPropertiesFromCP(String path) throws IOException {
		Properties props = new Properties();
		InputStream is = readFromCP(path);
		props.load(is);
		try { is.close(); } catch(Exception e) {}
		return props;
	}

	public static String readFileFromCPAsString(String path) throws IOException {
		InputStream is = readFromCP(path);
		if (is == null)
			return null;
		InputStreamReader isr = new InputStreamReader(is, FILE_ENCODING);
		try {
    		char[] buf = new char[1048 * 8];
    		StringBuilder sb = new StringBuilder();
    		while (true) {
    			int len = isr.read(buf);
    			if (len <= 0)
    				break;
    			sb.append(buf, 0, len);
    		}
    		return sb.toString();
		} finally {
			try { isr.close(); } catch(Exception e) {}
		}
	}

    public static LineNumberReader openFile(File file) throws IOException {
        InputStreamReader isr = new InputStreamReader(new FileInputStream(file), FILE_ENCODING);
        return new LineNumberReader(isr);
    }

    public static LineNumberReader openFileFromCP(String file) {
        InputStreamReader isr;
		try {
			InputStream is = readFromCP(file);
			if (is == null)
				return null;
			isr = new InputStreamReader(is, FILE_ENCODING);
		} catch (IOException e) {
			return null;
		}
        return new LineNumberReader(isr);
    }

    public static PrintWriter openFileToWrite(File pipeFile, boolean append) throws IOException {
		return new PrintWriter(new OutputStreamWriter(new FileOutputStream(pipeFile, append), "UTF8"));
    }

	public static String readFileAsString(File file) throws IOException {
		LineNumberReader lnr = openFile(file);
		StringBuilder sb = new StringBuilder();
		String line;
		while ((line = lnr.readLine()) != null) {
			if (sb.length() > 0)
				sb.append(NL);
			sb.append(line);
		}
		return sb.toString();
	}

    public static void setToClipboard(String value) {
    	Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(value), null);
    }

    /**
     * Remove the leading symbols except for space, dash and quotes.
     */
    public static String cleanseWord(String s) {
    	if (s == null)
    		return null;
    	s = s.trim();
    	int i;
    	for (i=0; i<s.length(); ++i) {
    		char c = s.charAt(i);
    		if (Character.isLetter(c) || c == ' ' || c == '-' || c == '\'' | c == '"')
    			break;
    	}
    	return (i > 0) ? s.substring(i) : s;
    }

	static final Comparator<String> longestFirstCptr = new Comparator<String>() {
		@Override public int compare(String a, String b) {
			int len1 = a.length();
			int len2 = b.length();
			if (len1 == len2)
				return 0;
			return (len1 > len2) ? -1 : 1;
		}
	};

	public static void sortByLength(List<String> lst) {
		lst.sort(longestFirstCptr);
    }

	public static void sortByLength(String[] arr) {
		Arrays.sort(arr, longestFirstCptr);
    }

    public static String[] _sortWords(Collection<String> words) {
		HashMap<String, Object> map = new HashMap<String, Object>(); // String or ArrayList<String>
		for (String w : words) {
			Object o = map.get(w);
			if (o == null)
				map.put(w, w);
			else {
				ArrayList<String> list;
				if (o instanceof String) {
					list = new ArrayList<String>();
					map.put(w, list);
				} else {
					list = (ArrayList<String>)o;
				}
				list.add(w);
			}
		}
		String[] keys = map.keySet().toArray(new String[map.size()]);
		Arrays.sort(keys);
		String[] ret = words.toArray(new String[words.size()]);
		int idx = 0;
		for (String k : keys) {
			Object o = map.get(k);
			if (o instanceof String)
				ret[idx++] = (String)o;
			else
				for (String s : (ArrayList<String>)o)
					ret[idx++] = s;
		}
		return ret;
    }

    /**
     * @param line values separated by spaces; can be quoted in single-quotes.
     */
    public static String[] parseArguments(String line) {
    	return parseCSV(line, ' ', '\'', true, -1);
    }

    /**
     * @param line Excel-style CSV values: separated by commas; can be quoted in double-quotes.
     */
    public static String[] parseCSV(String line, int maxCols) {
		return parseCSV(line, '"', ',', false, maxCols);
	}

	public static String[] parseCSV(String line, char separator, char quote, boolean skipEmpty, int maxCols) {
		final int START     = 0;
		final int IN_QUOTES = 1;
		final int SEPARATOR = 2;
		final int UNQUOTED  = 3;
		final int SKIP      = 4;

		ArrayList<String> list = new ArrayList<String>();
		int state = START;
		int startIdx = 0;
		for (int i=0; i<line.length(); ++i) {
			char c = line.charAt(i);
			switch(state) {
			case START:
				if (c == quote) {
					startIdx = i+1;
					state = IN_QUOTES;
				}
				else if (c == separator) {
					parseCSV_addToList(list, skipEmpty, line.substring(startIdx, i).trim());
					startIdx = i+1;
					state = SEPARATOR;
				}
				else {
					state = UNQUOTED;
				}
				break;
			case SEPARATOR:
				if (c == quote) {
					state = IN_QUOTES;
					startIdx = i+1;
				}
				else if (c == separator) {
					parseCSV_addToList(list, skipEmpty, "");
					startIdx = i+1;
				}
				else {
					state = UNQUOTED;
				}
				break;
			case IN_QUOTES:
				if (c == quote) {
					parseCSV_addToList(list, skipEmpty, line.substring(startIdx, i).trim());
					state = SKIP;
				}
				break;
			case UNQUOTED:
				if (c == separator) {
					parseCSV_addToList(list, skipEmpty, line.substring(startIdx, i).trim());
					state = SEPARATOR;
					startIdx = i+1;
				}
				break;
			case SKIP:
				if (c == separator) {
					state = SEPARATOR;
					startIdx = i+1;
				}
				break;
			}
			if (maxCols > 0 && list.size() >= maxCols)
				break;
		}

		switch(state) {
		case SEPARATOR:
			parseCSV_addToList(list, skipEmpty, "");
			break;
		case START:
		case IN_QUOTES:
		case UNQUOTED:
			parseCSV_addToList(list, skipEmpty, line.substring(startIdx).trim());
			break;
		}

		return list.toArray(new String[list.size()]);
	}

	private static void parseCSV_addToList(ArrayList<String> list, boolean skipEmpty, String value) {
		if (!skipEmpty || isNotEmpty(value))
			list.add(value);
	}

	// sa: string array shorthand
	public static String[] sa(String... strs) {
		return strs;
	}

	// sa: string array shorthand
	public static String[] sa(int len, String... strs) {
		if (strs.length != len) {
			String[] tmp = new String[len];
			len = min(len, strs.length);
			for (int i=0; i<len; ++i)
				tmp[i] = strs[i];
			strs = tmp;
		}
		return strs;
	}

	public static ImmutableStringList isl(String... strs) {
		return new ImmutableStringList(strs);
	}

	public static String replacerLast(String str, char ch) {
		return chopRight(str, 1) + ch;
	}

	public static String replacerLast(String str, String suffix) {
		return chopRight(str, 1) + suffix;
	}

	public static String chopRight(String str, int nombre) {
		if (str.length() <= nombre)
			return "";
		return str.substring(0, str.length() - nombre);
	}

	public static String chop(String str, int left, int right) {
		int idx2 = str.length() - right;
		if (idx2 < left)
			return str;
		return str.substring(left, idx2);
	}

	public static char last(String str) {
		if (str.length() > 0)
			return str.charAt(str.length()-1);
		return ' ';
	}

	public static String last(String str, int nombre) {
		if (nombre < str.length())
			return str.substring(str.length()-nombre);
		return str;
	}

	public static String[] prefix(String prefix, String[] sa) {
		return prefix(prefix, sa, true);
	}

	public static String[] prefix(String prefix, String[] sa, boolean local) {
		String[] result = sa;
		if (prefix != null && isNotEmpty(sa)) {
			if (!local)
				result = new String[sa.length];
			for (int i=sa.length-1; i>=0; --i)
				result[i] = prefix(prefix, sa[i]);
		}
		return result;
	}

	public static String prefix(String s1, String s2) {
		if (s2 == null) return null;
		if (s1 == null) return s2;
		return s1 + s2;
	}

	public static String[] append(String racine, ImmutableStringList suffices) {
		String [] resulte = new String[suffices.size()];
		for (int i=0; i<suffices.size(); ++i)
			resulte[i] = racine + neverNull(suffices.get(i));
		return resulte;
	}

	public static String[] append(String racine, String[] suffices) {
		String [] resulte = new String[suffices.length];
		for (int i=0; i<suffices.length; ++i)
			resulte[i] = racine + neverNull(suffices[i]);
		return resulte;
	}

	public static String[] append(ImmutableStringList racines, ImmutableStringList suffices) {
		String [] resulte = new String[suffices.size()];
		for (int i=0; i<suffices.size(); ++i)
			resulte[i] = racines.get(i) + neverNull(suffices.get(i));
		return resulte;
	}

	// Apache lang3 StringUtils
    public static String stripAccents(final String input) {
        if(input == null) {
            return null;
        }
        final Pattern pattern = Pattern.compile("\\p{InCombiningDiacriticalMarks}+");//$NON-NLS-1$
        final String decomposed = Normalizer.normalize(input, Normalizer.Form.NFD);
        // Note that this doesn't correctly remove ligatures...
        return pattern.matcher(decomposed).replaceAll("");//$NON-NLS-1$
    }

	// quoted string
    public static String qs(String[] sa, int idx) {
    	return (sa == null) || (idx >= sa.length) ? "null" : qs(sa[idx]);
    }

    public static String qs(String s) {
    	return isEmpty(s) ? "null" : ("\"" + s + '"');
    }
    
    // never null
	public static String nn(String[] sa, int idx) {
		return neverNull(at(sa, idx));
	}

	public static String nn(String[][] saa, int i1, int i2) {
		return neverNull(at(saa, i1, i2));
	}

    public static String neverEmptyAndTrim(String s) {
        if (isEmpty(s))
            return null;
        return s.trim();
    }

    public static boolean endsWith(String s, String[] suffices) {
    	for (String suf : suffices)
    		if (s.endsWith(suf))
    			return true;
    	return false;
    }

    /**
     * @param str can be a multi-line string with \n as the separator.
     */
    public static int getWidth(Object str) {
    	return (str == null) ? 0 : maxLen(0, str.toString().split("\n"));
    }

    public static String todayYYYYMMDD() {
    	Calendar cal = Calendar.getInstance();
    	String m = String.valueOf(cal.get(Calendar.MONTH) + 1);
    	String d = String.valueOf(cal.get(Calendar.DAY_OF_MONTH));
    	if (m.length() == 1)
    		m = "0" + m;
    	if (d.length() == 1)
    		d = "0" + d;
        return "" + cal.get(Calendar.YEAR) + '-' + m + '-' + d;
    }

    public static int getYear() {
    	final int year = Calendar.getInstance().get(Calendar.YEAR);
    	return year;
    }

	public static String combineSimpleDiffs(String v1, String v2) {
		if (isEmpty(v1)) return v2;
		if (isEmpty(v2)) return v1;

		// match from beginning and from end, and combine the diffs in the middle (or end)
		int len1 = v1.length();
		int len2 = v2.length();
		int sameHead = 0;
		int sameTail = 0;
		for (; sameHead < len1 && sameHead < len2 && v1.charAt(sameHead) == v2.charAt(sameHead); ++sameHead);
		for (; sameTail < len1 && sameTail < len2 && v1.charAt(len1-sameTail-1) == v2.charAt(len2-sameTail-1); ++sameTail);

		String diff;
		if (sameHead > 0) {
			String left = v1.substring(0, sameHead);
			if (sameTail == 0) {
				if (sameHead == len1)
					diff = v2.substring(sameHead);
				else if (sameHead == len2)
					diff = v1.substring(sameHead);
				else
					diff = v1.substring(sameHead) + '/' +  v2.substring(sameHead);
			} else {
				if (sameHead <= 3)
					return v1 + '/' + v2;
				if (sameHead + sameTail == len1)
					diff = chop(v2, sameHead, sameTail);
				else if (sameHead + sameTail == len2)
					diff = chop(v1, sameHead, sameTail);
				else
					diff = v1.substring(sameHead) + '/' + v2.substring(sameHead);
			}
			return left + '(' + diff + ')';
		}
		else {
			if (sameTail == len1)
				diff = chop(v2, 0, sameTail);
			else if (sameTail == len2)
				diff = chop(v1, 0, sameTail);
			else
				return v1 + '/' + v2;
			return "(" + diff + ')' + last(v1, sameTail);
		}
	}

//  public static void dumpStringBinary(String s) {
//  	for (byte b : s.getBytes()) {
//  		String x = Integer.toHexString(((int)b) & 0xFF);
//  		if (x.length() == 1)
//  			x = "0" + x;
//  		Console.println(x + " ");
//  	}
//  	Console.println("");
//  }

}
